"""Test the Tesla Fleet init."""

from copy import deepcopy
from datetime import timedelta
from unittest.mock import AsyncMock, patch

from aiohttp import RequestInfo
from aiohttp.client_exceptions import ClientResponseError
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from tesla_fleet_api.const import Scope, VehicleDataEndpoint
from tesla_fleet_api.exceptions import (
    InvalidRegion,
    InvalidToken,
    LibraryError,
    LoginRequired,
    OAuthExpired,
    RateLimited,
    TeslaFleetError,
    VehicleOffline,
)

from homeassistant.components.tesla_fleet.const import AUTHORIZE_URL
from homeassistant.components.tesla_fleet.coordinator import (
    ENERGY_HISTORY_INTERVAL,
    ENERGY_INTERVAL,
    ENERGY_INTERVAL_SECONDS,
    VEHICLE_INTERVAL,
    VEHICLE_INTERVAL_SECONDS,
    VEHICLE_WAIT,
)
from homeassistant.components.tesla_fleet.models import TeslaFleetData
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
    ImplementationUnavailableError,
)

from . import setup_platform
from .conftest import create_config_entry
from .const import VEHICLE_ASLEEP, VEHICLE_DATA_ALT

from tests.common import MockConfigEntry, async_fire_time_changed

ERRORS = [
    (InvalidToken, ConfigEntryState.SETUP_ERROR),
    (OAuthExpired, ConfigEntryState.SETUP_ERROR),
    (LoginRequired, ConfigEntryState.SETUP_ERROR),
    (TeslaFleetError, ConfigEntryState.SETUP_RETRY),
]


async def test_load_unload(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
) -> None:
    """Test load and unload."""

    await setup_platform(hass, normal_config_entry)

    assert normal_config_entry.state is ConfigEntryState.LOADED
    assert isinstance(normal_config_entry.runtime_data, TeslaFleetData)
    assert await hass.config_entries.async_unload(normal_config_entry.entry_id)
    await hass.async_block_till_done()
    assert normal_config_entry.state is ConfigEntryState.NOT_LOADED
    assert not hasattr(normal_config_entry, "runtime_data")


@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
async def test_init_error(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_products: AsyncMock,
    side_effect: TeslaFleetError,
    state: ConfigEntryState,
) -> None:
    """Test init with errors."""

    mock_products.side_effect = side_effect
    await setup_platform(hass, normal_config_entry)
    assert normal_config_entry.state is state


async def test_oauth_refresh_expired(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_products: AsyncMock,
) -> None:
    """Test init with expired Oauth token."""

    # Patch the token refresh to raise an error
    with patch(
        "homeassistant.components.tesla_fleet.OAuth2Session.async_ensure_token_valid",
        side_effect=ClientResponseError(
            RequestInfo(AUTHORIZE_URL, "POST", {}, AUTHORIZE_URL), None, status=401
        ),
    ) as mock_async_ensure_token_valid:
        # Trigger an unmocked function call
        mock_products.side_effect = InvalidRegion
        await setup_platform(hass, normal_config_entry)

        mock_async_ensure_token_valid.assert_called_once()
    assert normal_config_entry.state is ConfigEntryState.SETUP_ERROR


async def test_oauth_refresh_error(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_products: AsyncMock,
) -> None:
    """Test init with Oauth refresh failure."""

    # Patch the token refresh to raise an error
    with patch(
        "homeassistant.components.tesla_fleet.OAuth2Session.async_ensure_token_valid",
        side_effect=ClientResponseError(
            RequestInfo(AUTHORIZE_URL, "POST", {}, AUTHORIZE_URL), None, status=400
        ),
    ) as mock_async_ensure_token_valid:
        # Trigger an unmocked function call
        mock_products.side_effect = InvalidRegion
        await setup_platform(hass, normal_config_entry)

        mock_async_ensure_token_valid.assert_called_once()
    assert normal_config_entry.state is ConfigEntryState.SETUP_RETRY


# Test devices
async def test_devices(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    device_registry: dr.DeviceRegistry,
    snapshot: SnapshotAssertion,
) -> None:
    """Test device registry."""
    await setup_platform(hass, normal_config_entry)
    devices = dr.async_entries_for_config_entry(
        device_registry, normal_config_entry.entry_id
    )

    for device in devices:
        assert device == snapshot(name=f"{device.identifiers}")


# Vehicle Coordinator
async def test_vehicle_refresh_offline(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_vehicle_state: AsyncMock,
    mock_vehicle_data: AsyncMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator refresh with an error."""
    await setup_platform(hass, normal_config_entry)
    assert normal_config_entry.state is ConfigEntryState.LOADED

    mock_vehicle_state.assert_called_once()
    mock_vehicle_data.assert_called_once()
    mock_vehicle_state.reset_mock()
    mock_vehicle_data.reset_mock()

    # Then the vehicle goes offline despite saying its online
    mock_vehicle_data.side_effect = VehicleOffline
    freezer.tick(VEHICLE_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    mock_vehicle_state.assert_called_once()
    mock_vehicle_data.assert_called_once()
    mock_vehicle_state.reset_mock()
    mock_vehicle_data.reset_mock()

    # And stays offline
    mock_vehicle_state.return_value = VEHICLE_ASLEEP
    freezer.tick(VEHICLE_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    mock_vehicle_state.assert_called_once()
    mock_vehicle_data.assert_not_called()


@pytest.mark.parametrize(("side_effect"), ERRORS)
async def test_vehicle_refresh_error(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_vehicle_data: AsyncMock,
    side_effect: TeslaFleetError,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator refresh makes entity unavailable."""

    await setup_platform(hass, normal_config_entry)

    mock_vehicle_data.side_effect = side_effect
    freezer.tick(VEHICLE_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert (state := hass.states.get("sensor.test_battery_level"))
    assert state.state == "unavailable"


async def test_vehicle_refresh_ratelimited(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_vehicle_data: AsyncMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator refresh handles 429."""

    mock_vehicle_data.side_effect = RateLimited(
        {"after": VEHICLE_INTERVAL_SECONDS + 10}
    )
    await setup_platform(hass, normal_config_entry)

    assert (state := hass.states.get("sensor.test_battery_level"))
    assert state.state == "unknown"

    mock_vehicle_data.reset_mock()

    freezer.tick(VEHICLE_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert (state := hass.states.get("sensor.test_battery_level"))
    assert state.state == "unknown"


async def test_vehicle_sleep(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_vehicle_data: AsyncMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator refresh with an error."""

    TEST_INTERVAL = timedelta(seconds=120)

    with patch(
        "homeassistant.components.tesla_fleet.coordinator.VEHICLE_INTERVAL",
        TEST_INTERVAL,
    ):
        await setup_platform(hass, normal_config_entry)
        assert mock_vehicle_data.call_count == 1

        freezer.tick(VEHICLE_WAIT + TEST_INTERVAL)
        async_fire_time_changed(hass)
        # Let vehicle sleep, no updates for 15 minutes
        await hass.async_block_till_done()
        assert mock_vehicle_data.call_count == 2

        freezer.tick(TEST_INTERVAL)
        async_fire_time_changed(hass)
        # No polling, call_count should not increase
        await hass.async_block_till_done()
        assert mock_vehicle_data.call_count == 2

        freezer.tick(VEHICLE_WAIT)
        async_fire_time_changed(hass)
        # Vehicle didn't sleep, go back to normal
        await hass.async_block_till_done()
        assert mock_vehicle_data.call_count == 3

        freezer.tick(TEST_INTERVAL)
        async_fire_time_changed(hass)
        # Regular polling
        await hass.async_block_till_done()
        assert mock_vehicle_data.call_count == 4

        mock_vehicle_data.return_value = VEHICLE_DATA_ALT
        freezer.tick(TEST_INTERVAL)
        async_fire_time_changed(hass)
        # Vehicle active
        await hass.async_block_till_done()
        assert mock_vehicle_data.call_count == 5

        freezer.tick(TEST_INTERVAL)
        async_fire_time_changed(hass)
        # Dont let sleep when active
        await hass.async_block_till_done()
        assert mock_vehicle_data.call_count == 6

        freezer.tick(TEST_INTERVAL)
        async_fire_time_changed(hass)
        # Dont let sleep when active
        await hass.async_block_till_done()
        assert mock_vehicle_data.call_count == 7


# Test Energy Live Coordinator
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
async def test_energy_live_refresh_error(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_live_status: AsyncMock,
    side_effect: TeslaFleetError,
    state: ConfigEntryState,
) -> None:
    """Test coordinator refresh with an error."""
    mock_live_status.side_effect = side_effect
    await setup_platform(hass, normal_config_entry)
    assert normal_config_entry.state is state


# Test Energy Site Coordinator
@pytest.mark.parametrize(("side_effect", "state"), ERRORS)
async def test_energy_site_refresh_error(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_site_info: AsyncMock,
    side_effect: TeslaFleetError,
    state: ConfigEntryState,
) -> None:
    """Test coordinator refresh with an error."""
    mock_site_info.side_effect = side_effect
    await setup_platform(hass, normal_config_entry)
    assert normal_config_entry.state is state


# Test Energy History Coordinator
@pytest.mark.parametrize(("side_effect"), [side_effect for side_effect, _ in ERRORS])
async def test_energy_history_refresh_error(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_energy_history: AsyncMock,
    side_effect: TeslaFleetError,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator refresh with an error."""
    await setup_platform(hass, normal_config_entry)
    assert normal_config_entry.state is ConfigEntryState.LOADED

    # Now test that the coordinator handles errors during refresh
    mock_energy_history.side_effect = side_effect
    freezer.tick(ENERGY_HISTORY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    # The coordinator should handle the error gracefully
    assert normal_config_entry.state is ConfigEntryState.LOADED


async def test_energy_live_refresh_ratelimited(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_live_status,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator refresh handles 429."""

    await setup_platform(hass, normal_config_entry)

    mock_live_status.side_effect = RateLimited({"after": ENERGY_INTERVAL_SECONDS + 10})
    freezer.tick(ENERGY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert mock_live_status.call_count == 2

    freezer.tick(ENERGY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    # Should not call for another 10 seconds
    assert mock_live_status.call_count == 2

    freezer.tick(ENERGY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert mock_live_status.call_count == 3


async def test_energy_info_refresh_ratelimited(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_site_info: AsyncMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator refresh handles 429."""

    await setup_platform(hass, normal_config_entry)

    mock_site_info.side_effect = RateLimited({"after": ENERGY_INTERVAL_SECONDS + 10})
    freezer.tick(ENERGY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert mock_site_info.call_count == 2

    freezer.tick(ENERGY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    # Should not call for another 10 seconds
    assert mock_site_info.call_count == 2

    freezer.tick(ENERGY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert mock_site_info.call_count == 3


async def test_energy_history_refresh_ratelimited(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_energy_history: AsyncMock,
    freezer: FrozenDateTimeFactory,
) -> None:
    """Test coordinator refresh handles 429."""

    await setup_platform(hass, normal_config_entry)

    mock_energy_history.side_effect = RateLimited(
        {"after": int(ENERGY_HISTORY_INTERVAL.total_seconds() + 10)}
    )
    freezer.tick(ENERGY_HISTORY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert mock_energy_history.call_count == 1

    freezer.tick(ENERGY_HISTORY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    # Should not call for another 10 seconds
    assert mock_energy_history.call_count == 1

    freezer.tick(ENERGY_HISTORY_INTERVAL)
    async_fire_time_changed(hass)
    await hass.async_block_till_done()

    assert mock_energy_history.call_count == 2


async def test_init_region_issue(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_products: AsyncMock,
    mock_find_server: AsyncMock,
) -> None:
    """Test init with region issue."""

    mock_products.side_effect = InvalidRegion
    await setup_platform(hass, normal_config_entry)
    mock_find_server.assert_called_once()
    assert normal_config_entry.state is ConfigEntryState.SETUP_RETRY


async def test_init_region_issue_failed(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_products: AsyncMock,
    mock_find_server: AsyncMock,
) -> None:
    """Test init with unresolvable region issue."""

    mock_products.side_effect = InvalidRegion
    mock_find_server.side_effect = LibraryError
    await setup_platform(hass, normal_config_entry)
    mock_find_server.assert_called_once()
    assert normal_config_entry.state is ConfigEntryState.SETUP_ERROR


async def test_signing(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_products: AsyncMock,
) -> None:
    """Tests when a vehicle requires signing."""

    # Make the vehicle require command signing
    products = deepcopy(mock_products.return_value)
    products["response"][0]["command_signing"] = "required"
    mock_products.return_value = products

    with patch(
        "homeassistant.components.tesla_fleet.TeslaFleetApi.get_private_key"
    ) as mock_get_private_key:
        await setup_platform(hass, normal_config_entry)
        mock_get_private_key.assert_called_once()


async def test_bad_implementation(
    hass: HomeAssistant,
    bad_config_entry: MockConfigEntry,
) -> None:
    """Test handling of a bad authentication implementation."""

    await setup_platform(hass, bad_config_entry)
    assert bad_config_entry.state is ConfigEntryState.SETUP_ERROR

    # Ensure reauth flow starts
    assert any(bad_config_entry.async_get_active_flows(hass, {"reauth"}))
    result = await bad_config_entry.start_reauth_flow(hass)
    assert result["type"] is FlowResultType.FORM
    assert result["step_id"] == "reauth_confirm"
    assert not result["errors"]


async def test_vehicle_without_location_scope(
    hass: HomeAssistant,
    expires_at: int,
    mock_vehicle_data: AsyncMock,
) -> None:
    """Test vehicle setup without VEHICLE_LOCATION scope excludes location endpoint."""

    # Create config entry without VEHICLE_LOCATION scope
    config_entry = create_config_entry(
        expires_at,
        [
            Scope.OPENID,
            Scope.OFFLINE_ACCESS,
            Scope.VEHICLE_DEVICE_DATA,
            # Deliberately exclude Scope.VEHICLE_LOCATION
        ],
    )

    await setup_platform(hass, config_entry)
    assert config_entry.state is ConfigEntryState.LOADED

    # Verify that vehicle_data was called without LOCATION_DATA endpoint
    mock_vehicle_data.assert_called()
    call_args = mock_vehicle_data.call_args
    endpoints = call_args.kwargs.get("endpoints", [])

    # Should not include LOCATION_DATA endpoint
    assert VehicleDataEndpoint.LOCATION_DATA not in endpoints

    # Should include other endpoints
    assert VehicleDataEndpoint.CHARGE_STATE in endpoints
    assert VehicleDataEndpoint.CLIMATE_STATE in endpoints
    assert VehicleDataEndpoint.DRIVE_STATE in endpoints
    assert VehicleDataEndpoint.VEHICLE_STATE in endpoints
    assert VehicleDataEndpoint.VEHICLE_CONFIG in endpoints


async def test_vehicle_with_location_scope(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
    mock_vehicle_data: AsyncMock,
) -> None:
    """Test vehicle setup with VEHICLE_LOCATION scope includes location endpoint."""
    await setup_platform(hass, normal_config_entry)
    assert normal_config_entry.state is ConfigEntryState.LOADED

    # Verify that vehicle_data was called with LOCATION_DATA endpoint
    mock_vehicle_data.assert_called()
    call_args = mock_vehicle_data.call_args
    endpoints = call_args.kwargs.get("endpoints", [])

    # Should include LOCATION_DATA endpoint when scope is present
    assert VehicleDataEndpoint.LOCATION_DATA in endpoints

    # Should include all other endpoints
    assert VehicleDataEndpoint.CHARGE_STATE in endpoints
    assert VehicleDataEndpoint.CLIMATE_STATE in endpoints
    assert VehicleDataEndpoint.DRIVE_STATE in endpoints
    assert VehicleDataEndpoint.VEHICLE_STATE in endpoints
    assert VehicleDataEndpoint.VEHICLE_CONFIG in endpoints


async def test_oauth_implementation_not_available(
    hass: HomeAssistant,
    normal_config_entry: MockConfigEntry,
) -> None:
    """Test that unavailable OAuth implementation raises ConfigEntryNotReady."""
    normal_config_entry.add_to_hass(hass)

    with patch(
        "homeassistant.components.tesla_fleet.async_get_config_entry_implementation",
        side_effect=ImplementationUnavailableError,
    ):
        await hass.config_entries.async_setup(normal_config_entry.entry_id)
        await hass.async_block_till_done()

    assert normal_config_entry.state is ConfigEntryState.SETUP_RETRY
